依 Day 27~28 已經進入 RecyclerView、Firebase、Fragment,我建議 Day 29 接著把 Fragment 往「真正 App 架構」推進,主題放在 Fragment 之間切換 + BottomNavigationView,也能自然銜接後面的完整專案。Android 官方 Fragment 文件: Android Developers — Fragments
Day 29|Android Fragment 實戰進階:BottomNavigationView 打造 App 底部導覽列
前一天我們已經認識了 Fragment,也實際建立 Fragment1、Fragment2、Fragment3,開始把一個 Activity 拆成不同功能畫面。
但做到這裡會遇到下一個問題:
Fragment 都建立好了,使用者到底要怎麼切換?
今天就來完成 Android App 很常見的一種介面:
BottomNavigationView + Fragment
也就是我們在許多 App 底部看到的導覽列。
⸻
一、今天要完成什麼?
今天預計完成這樣的 App 架構:
┌─────────────────────────────┐
│ │
│ │
│ Fragment 畫面 │
│ │
│ Fragment1 / 2 / 3 │
│ │
├─────────────────────────────┤
│ 🏠 📋 👤 │
│ 首頁 資料 我的 │
└─────────────────────────────┘
使用者按下底部不同按鈕:
首頁
↓
Fragment1
資料
↓
Fragment2
我的
↓
Fragment3
整個過程不需要一直開新的 Activity。
⸻
二、Activity 與 Fragment 再複習一次
前面我們學 Android 時,大部分畫面都是使用:
MainActivity
SelectActivity
FirebaseActivity
...
例如:
val intent = Intent(this, SelectActivity::class.java)
startActivity(intent)
這代表:
MainActivity
│
└──── Intent ────▶ SelectActivity
每切換一個主要畫面,就可能建立另一個 Activity。
但是 Fragment 的概念不同。
一個 Activity 裡面可以放很多 Fragment:
MainActivity
│
├── Fragment1
├── Fragment2
└── Fragment3
所以 MainActivity 比較像是一個「容器」。
真正顯示給使用者看的內容,可以交給 Fragment。
⸻
三、什麼是 Fragment?
Fragment 可以把它想像成:
Activity 裡面的一小塊 UI 與功能模組。
例如一個 App:
MainActivity
│
├── HomeFragment
│
├── DataFragment
│
└── ProfileFragment
當使用者切換頁面時:
MainActivity 不換
│
▼
只替換 Fragment
因此畫面的結構會變成:
┌──────────────────────────┐
│ MainActivity │
│ │
│ ┌──────────────────────┐ │
│ │ │ │
│ │ Fragment │ │
│ │ │ │
│ └──────────────────────┘ │
│ │
│ BottomNavigationView │
└──────────────────────────┘
這也是為什麼 Fragment 很適合拿來製作 App 的主要分頁。
⸻
四、今天的三個 Fragment
延續昨天的程式,我們可以準備:
Fragment1
Fragment2
Fragment3
例如:
class Fragment1 : Fragment(R.layout.fragment_1) {
}
第二個:
class Fragment2 : Fragment(R.layout.fragment_2) {
}
第三個:
class Fragment3 : Fragment(R.layout.fragment_3) {
}
每一個 Fragment 都有自己的 Layout:
Fragment1
↓
fragment_1.xml
Fragment2
↓
fragment_2.xml
Fragment3
↓
fragment_3.xml
這樣每個 Fragment 就能負責自己的畫面。
⸻
五、建立 Fragment 顯示區域
接下來 MainActivity 必須留一塊地方給 Fragment。
例如:
可以把 FrameLayout 想成:
┌──────────────────────────┐
│ │
│ │
│ fragmentContainer │
│ │
│ │
└──────────────────────────┘
Fragment1、Fragment2、Fragment3 都會輪流顯示在這裡。
⸻
六、FragmentManager 登場
要控制 Fragment,就會使用:
supportFragmentManager
它就是 Fragment 的管理者。
例如我們要把 Fragment1 放進 fragmentContainer:
supportFragmentManager
.beginTransaction()
.replace(R.id.fragmentContainer, Fragment1())
.commit()
拆開來看:
supportFragmentManager
取得 FragmentManager。
接著:
.beginTransaction()
開始一次 Fragment 畫面操作。
然後:
.replace(
R.id.fragmentContainer,
Fragment1()
)
把指定的 Fragment 放進容器。
最後:
.commit()
正式執行這次 FragmentTransaction。
所以完整流程就是:
supportFragmentManager
↓
beginTransaction()
↓
replace()
↓
commit()
⸻
七、把切換 Fragment 包成函式
如果每次切換都寫:
supportFragmentManager
.beginTransaction()
.replace(R.id.fragmentContainer, Fragment1())
.commit()
程式很快就會出現很多重複內容。
因此可以整理成:
private fun replaceFragment(fragment: Fragment) {
supportFragmentManager
.beginTransaction()
.replace(R.id.fragmentContainer, fragment)
.commit()
}
之後只需要:
replaceFragment(Fragment1())
或:
replaceFragment(Fragment2())
或:
replaceFragment(Fragment3())
程式會乾淨很多。
⸻
八、BottomNavigationView 是什麼?
BottomNavigationView 就是 Android 常見的底部導覽列。
例如:
┌─────────────────────────────┐
│ │
│ App Content │
│ │
├─────────────────────────────┤
│ 🏠 📋 👤 │
│ 首頁 資料 我的 │
└─────────────────────────────┘
它通常適合放:
3~5 個主要功能。
例如:
首頁
搜尋
通知
收藏
個人資料
今天我們使用三個:
首頁
資料
我的
⸻
九、建立 Bottom Navigation Menu
建立:
res
└── menu
└── bottom_menu.xml
內容:
這裡定義三個按鈕:
nav_home
nav_data
nav_profile
接下來再把它交給 BottomNavigationView。
⸻
十、加入 BottomNavigationView
在 activity_main.xml 加入:
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_menu" />
其中最重要的是:
app:menu="@menu/bottom_menu"
也就是告訴 BottomNavigationView:
你的按鈕內容放在 bottom_menu.xml。
因此:
bottom_menu.xml
│
▼
BottomNavigationView
│
├── 首頁
├── 資料
└── 我的
⸻
十一、監聽 BottomNavigationView
接下來在 Kotlin 裡取得:
val bottomNavigationView =
findViewById(
R.id.bottomNavigationView
)
接著設定:
bottomNavigationView
.setOnItemSelectedListener { item ->
when (item.itemId) {
R.id.nav_home -> {
replaceFragment(Fragment1())
true
}
R.id.nav_data -> {
replaceFragment(Fragment2())
true
}
R.id.nav_profile -> {
replaceFragment(Fragment3())
true
}
else -> false
}
}
這段就是今天最重要的核心。
⸻
十二、完整切換流程
當使用者按下:
🏠 首頁
會觸發:
R.id.nav_home
然後:
replaceFragment(Fragment1())
最後:
MainActivity
│
▼
FragmentManager
│
▼
replace()
│
▼
Fragment1
如果按下:
📋 資料
則變成:
MainActivity
│
▼
FragmentManager
│
▼
Fragment2
再按:
👤 我的
變成:
MainActivity
│
▼
FragmentManager
│
▼
Fragment3
這樣就完成一個基本的 App 分頁架構。
⸻
十三、MainActivity 完整範例
整理之後:
package com.example.fragmentdemo
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import com.google.android.material.bottomnavigation.BottomNavigationView
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 載入 MainActivity 的版面配置
setContentView(R.layout.activity_main)
// 第一次進入 Activity 時顯示首頁 Fragment。
// savedInstanceState != null 通常代表 Activity 正在重建,
// 此時 FragmentManager 會協助還原原本的 Fragment,
// 因此不要再建立一次 Fragment1。
if (savedInstanceState == null) {
replaceFragment(Fragment1())
}
// 取得底部導覽列
val bottomNavigationView =
findViewById(
R.id.bottomNavigationView
)
// 監聽使用者點擊底部導覽列
bottomNavigationView.setOnItemSelectedListener { item ->
when (item.itemId) {
// 首頁
R.id.nav_home -> {
// 顯示 Fragment1
replaceFragment(Fragment1())
// true 代表已處理這次點擊事件
true
}
// 資料頁
R.id.nav_data -> {
// 顯示 Fragment2
replaceFragment(Fragment2())
true
}
// 個人頁面
R.id.nav_profile -> {
// 顯示 Fragment3
replaceFragment(Fragment3())
true
}
// 如果沒有符合的 Menu ID,就不處理
else -> false
}
}
}
/**
* 切換 Fragment 的共用函式。
*
* fragment:
* 要顯示在 fragmentContainer 裡面的 Fragment。
*/
private fun replaceFragment(fragment: Fragment) {
// 取得 FragmentManager,
// 開始 FragmentTransaction。
supportFragmentManager
.beginTransaction()
// 將 fragmentContainer 目前顯示的 Fragment
// 替換成傳入的新 Fragment。
.replace(
R.id.fragmentContainer,
fragment
)
// 提交這次 FragmentTransaction。
.commit()
}
}
這樣 MainActivity 的責任就很明確:
MainActivity
│
├── 管理 BottomNavigationView
│
└── 管理 Fragment 切換
⸻
十四、為什麼不要全部塞在 MainActivity?
假設我們沒有 Fragment。
一開始 MainActivity 只有:
登入
後來加入:
Firebase
RecyclerView
個人資料
設定
收藏
搜尋
MainActivity 很可能慢慢變成:
MainActivity.kt
100 行
300 行
500 行
800 行
1000 行...
最後什麼功能都放在同一個 Activity。
這樣程式會越來越難維護。
使用 Fragment 後可以拆成:
MainActivity
│
├── HomeFragment
│
├── FirebaseFragment
├── ListFragment
├── ProfileFragment
└── SettingFragment
每一個 Fragment 負責自己的功能。
這就是 Android App 開始走向模組化的重要一步。
⸻
十五、把前幾天學過的東西放進 Fragment
這也是 Fragment 真正有趣的地方。
前面我們已經學過:
RecyclerView
Room
Firebase
Intent
SQLite
感測器
現在這些東西都可以開始整合。
例如:
MainActivity
│
├── HomeFragment
│
│ └── 首頁資訊
│
├── FirebaseFragment
│
│ └── Firebase 資料
│
└── ListFragment
└── RecyclerView
甚至:
Firebase
↓
Fragment
↓
RecyclerView
↓
Adapter
↓
CardView
這時候前面每天學的技術,就不再是一個一個分開的小範例,而是開始可以組合成真正的 App。
⸻
十六、Activity 與 Fragment 怎麼選?
可以簡單這樣理解:
功能 Activity Fragment
App 主入口 ✔
完全獨立流程 ✔
同一畫面的分頁 ✔
Bottom Navigation ✔
Tab 分頁 ✔
可重複 UI 模組 ✔
App 主要功能頁 ✔ ✔
不是說用了 Fragment 就不能用 Activity。
真正的 App 通常會兩者一起使用。
例如:
LoginActivity
│
▼
MainActivity
│
├── HomeFragment
├── DataFragment
└── ProfileFragment
登入可以是一個 Activity。
登入成功之後進入 MainActivity。
MainActivity 裡面再利用 Fragment 管理主要功能。
⸻
十七、今天的 App 架構
把今天的內容整理起來:
MainActivity
│
┌─────────────┴─────────────┐
│ │
▼ ▼
fragmentContainer BottomNavigationView
│ │
│ ┌───────────┼───────────┐
│ │ │ │
▼ ▼ ▼ ▼
Fragment nav_home nav_data nav_profile
│ │ │ │
│ ▼ ▼ ▼
│ Fragment1 Fragment2 Fragment3
│ │ │ │
└───────────────┴───────────┴───────────┘
│
▼
FragmentManager
│
▼
replace()
這就是今天最重要的觀念:
Activity 負責承載,Fragment 負責畫面,BottomNavigationView 負責讓使用者選擇要看的 Fragment。
⸻
十八、Fragment 與 Activity 最大差異
Fragment 最大的特色之一,就是它本身不是完全獨立存在。
Fragment 必須依附在 Activity 上。
所以:
Activity
│
└── Fragment
而不是:
Fragment
│
└── Activity
Fragment 也有自己的生命週期,例如:
onCreate()
↓
onCreateView()
↓
onViewCreated()
↓
onStart()
↓
onResume()
離開畫面時:
onPause()
↓
onStop()
↓
onDestroyView()
↓
onDestroy()
這裡有一個很重要的觀念:
Fragment 本身的生命週期與 Fragment View 的生命週期並不完全相同。
這件事情之後做到 ViewBinding、LiveData 或 Navigation Component 時會非常重要。
⸻
十九、今天學到什麼?
今天把昨天建立好的 Fragment 真正拿來使用。
從:
建立 Fragment
進一步做到:
建立 Fragment
↓
FragmentManager
↓
FragmentTransaction
↓
replace()
↓
BottomNavigationView
↓
Fragment1 / Fragment2 / Fragment3
也正式從:
單一 Activity 範例程式
開始走向:
真正 Android App 的多頁面架構
⸻
二十、今日重點整理
今天最重要的幾個關鍵字:
Fragment
FragmentManager
FragmentTransaction
replace()
commit()
FrameLayout
BottomNavigationView
Menu
setOnItemSelectedListener
其中 Fragment 切換最核心的程式就是:
supportFragmentManager
.beginTransaction()
.replace(
R.id.fragmentContainer,
fragment
)
.commit()
BottomNavigationView 則負責決定:
使用者按哪一個按鈕
↓
顯示哪一個 Fragment
最後形成:
MainActivity
│
▼
BottomNavigationView
│
┌─────────┼─────────┐
▼ ▼ ▼
Fragment1 Fragment2 Fragment3
首頁 資料 我的
到這裡,我們已經不只是會「做一個 Android 畫面」,而是開始思考:
一個 App 的不同功能,應該怎麼拆、怎麼組合、怎麼讓使用者切換。
這也是從 Android 基礎練習走向完整 App 開發很重要的一步。
⸻
參考資料
Android Developers — Fragments
https://developer.android.com/guide/fragments
Android Developers — FragmentManager
https://developer.android.com/guide/fragments/fragmentmanager
Android Developers — Fragment lifecycle
https://developer.android.com/guide/fragments/lifecycle
Material Design — Bottom navigation
https://m3.material.io/components/navigation-bar/overview
Material Components Android
https://github.com/material-components/material-components-android
這一篇很適合接在前一天的 Fragment1 / Fragment2 / Fragment3 後面;**Day 30 就可以把 RecyclerView + Firebase + Fragment + BottomNavigationView 全部合起來,當成 30 天系列的整合實戰收尾。**參考: Android Developers — FragmentManager Material Design — Navigation Bar
如果要維持你前幾天「圖文並茂」的版型,我也可以把這篇再改成 iThome 可直接貼上的 Markdown 版,並直接生成 Day 29 首圖+Fragment 切換架構圖。